/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     ybonnel - initial API and implementation
 */
package jsmc.moteurdb;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import jsmc.moteurdb.annotation.BaliseDb;
import jsmc.moteurdb.annotation.FichierDb;
import jsmc.moteurdb.exception.MoteurDbException;
import jsmc.moteurdb.factory.AbstractReaderDb;
import jsmc.moteurdb.factory.AbstractWriterDb;
import jsmc.moteurdb.factory.DefaultGestionnaireDbFactory;
import jsmc.moteurdb.factory.GestionnaireDbFactory;
import jsmc.moteurdb.modele.ChampDb;
import jsmc.moteurdb.modele.ClassDb;
import jsmc.moteurdb.modele.InsertInList;
import jsmc.moteurdb.modele.InsertObject;
import jsmc.opendata.nantes.util.NeoLog;

/**
 * Moteur de lecture et écriture de fichier CSV.<br/>
 * Voici des exemple d'utilisation :
 * <ul>
 * <li>Construction du moteur :<br/>
 * {@code MoteurDb moteur = new MoteurDb(ObjetDb.class);}</li>
 * <li>Lecture d'un fichier CSV :<br/>
 * {@code List<ObjetDb> objets = moteur.parseInputStream(stream, ObjetDb.class);}
 * </li>
 * <li>Ecriture d'un fichier CSV :<br/>
 * {@code moteur.writeFile(new FileWriter(file), objets, ObjetDb.class);}</li>
 * </ul>
 * 
 * @author ybonnel
 * 
 */
public class MoteurDb {

	/**
	 * Logger.
	 */
	private static final Logger LOGGER = Logger.getLogger("MoteurDb");

	/**
	 * Map des classes gérées.
	 */
	private final Map<Class<?>, ClassDb> mapClasses = new HashMap<Class<?>, ClassDb>();

	/**
	 * Entete courante.
	 */
	private String[] enteteCourante;

	/**
	 * Class courante.
	 */
	private ClassDb classCourante;

	/**
	 * Factory fournissant les reader et writer csv.
	 */
	private GestionnaireDbFactory factory;

	/**
	 * Permet d'utiliser autre chose que open-csv pour la lecture et écriture.
	 * 
	 * @param factory
	 *            factory autre que la factory par défaut.
	 */
	public void setFactory(GestionnaireDbFactory factory) {
		this.factory = factory;
	}

	/**
	 * Constructeur du moteur.
	 * 
	 * @param classes
	 *            liste des classes à gérer.
	 */
	public MoteurDb(List<Class<?>> classes) {
		factory = new DefaultGestionnaireDbFactory();
		for (Class<?> clazz : classes) {
			scannerClass(clazz);
		}
	}

	/**
	 * Lecteur CSV.
	 */
	private AbstractReaderDb lecteurDb;

	/**
	 * Crée un objet à partir de la ligne courante du csv.
	 * {@link MoteurDb#nouveauFichier(Reader, Class)} doit être appelé avant.
	 * 
	 * @return l'ojet créer.
	 */
	protected Object creerObjet() {
		if (classCourante == null) {
			throw new MoteurDbException(
					"La méthode creerObjet a étée appelée sans que la méthode nouveauFichier n'est été appelée.");
		}
		String[] champs = null;
		try {
			champs = lecteurDb.readLine();
			if (champs == null) {
				return null;
			}
			Object objetDb = classCourante.getContructeur().newInstance((Object[]) null);
			for (int numChamp = 0; numChamp < champs.length; numChamp++) {
				String champ = champs[numChamp];
				if (champ != null && !"".equals(champ)) {
					String nomChamp = enteteCourante[numChamp];
					ChampDb champDb = classCourante.getChampDb(nomChamp);
					if (champDb != null) {
						champDb.getField().setAccessible(true);
						champDb.getField().set(objetDb, champDb.getNewAdapterDb().parse(champ));
						champDb.getField().setAccessible(false);
					}
				}
			}
			return objetDb;
		} catch (Exception e) {
			throw new MoteurDbException("Erreur à l'instanciation de la class "
					+ classCourante.getClazz().getSimpleName() + " pour la ligne " + champs, e);
		}
	}

	/**
	 * Démarre la lecture d'une nouveau fichier.
	 * 
	 * @param reader
	 *            le fichier.
	 * @param clazz
	 *            la class associée.
	 */
	protected void nouveauFichier(Reader reader, Class<?> clazz) {
		NeoLog.v("MoteurDb","nouveauFichier");
		
		classCourante = mapClasses.get(clazz);
		if (classCourante == null) {
			
			throw new MoteurDbException("La class " + clazz.getSimpleName() + " n'est pas gérée");
		}
		
		lecteurDb = factory.createReaderDb(reader, classCourante.getSeparateurWithoutEscape());
		
		try {
			enteteCourante = lecteurDb.readLine();
			NeoLog.v("MoteurDb","nouveauFichier createReaderDb: " + enteteCourante[0] + " "+enteteCourante[1]);
		} catch (IOException e) {
			NeoLog.v("MoteurDb","nouveauFichier exception "+e.getMessage());
			throw new MoteurDbException(e);
		}
		if (Character.isIdentifierIgnorable(enteteCourante[0].charAt(0))) {
			NeoLog.v("MoteurDb","nouveauFichier entete courante:"+enteteCourante[0].substring(1));
			enteteCourante[0] = enteteCourante[0].substring(1);
		}
	}

	/**
	 * Ferme le lecteur courant.
	 */
	private void closeLecteurCourant() {
		if (lecteurDb != null) {
			try {
				lecteurDb.close();
			} catch (IOException exception) {
				LOGGER.log(Level.WARNING, "Erreur lors de la fermeture du LecteurDb", exception);
			}
			lecteurDb = null;
		}
	}

	/**
	 * Parse un inputStream représentant un fichier CSV pour le transformer en
	 * liste de <Objet>.
	 * 
	 * @param <Objet>
	 *            Objet associé au CSV.
	 * @param intputStream
	 *            inputStream représentant le fichier CSV.
	 * @param clazz
	 *            classe de l'objet associé au CSV.
	 * @return la liste d'<Objet> représentant les enregistrements du fichier
	 *         CSV.
	 */
	public <Objet> List<Objet> parseInputStream(InputStream intputStream, Class<Objet> clazz) {
		List<Objet> objets = new ArrayList<Objet>();
		parseFileAndInsert(new BufferedReader(new InputStreamReader(intputStream)), clazz, new InsertInList<Objet>(
				objets));
		return objets;
	}

	/**
	 * Permet de parser un fichier CSV tout en effectuant un traitement à chaque
	 * enregistrement.
	 * 
	 * @param reader
	 *            le Reader représentant le fichier CSV.
	 * @param clazz
	 *            classe de l'objet associé au CSV.
	 * @param <Objet>
	 *            Objet associé au CSV.
	 * @param insert
	 *            traitement à éffectuer pour chaque enregistrement.
	 */
	@SuppressWarnings("unchecked")
	public <Objet> void parseFileAndInsert(Reader reader, Class<Objet> clazz, InsertObject<Objet> insert) {
		int c = 0;
		NeoLog.v("MoteurDb","parseFileAndInsert");
		nouveauFichier(reader, clazz);
		Objet objet = (Objet) creerObjet();
		while (objet != null) {
			c++;
			//NeoLog.v("MoteurDb","parseFileAndInsert iteration");
			insert.insertObject(objet);
			objet = (Objet) creerObjet();
		}
		NeoLog.v("MoteurDb","parseFileAndInsert "+c+" lines");
		closeLecteurCourant();
	}

	/**
	 * Scanne une class pour la gérer dans le moteur.
	 * 
	 * @param clazz
	 *            classe à scanner.
	 */
	protected void scannerClass(Class<?> clazz) {
		FichierDb fichierDb = clazz.getAnnotation(FichierDb.class);
		if (fichierDb == null) {
			throw new MoteurDbException("Annotation FichierDb non présente sur la classe " + clazz.getSimpleName());
		}
		if (mapClasses.get(clazz) != null) {
			return;
		}
		ClassDb classDb = new ClassDb(fichierDb.separateur(), clazz);
		for (Field field : clazz.getDeclaredFields()) {
			BaliseDb baliseDb = field.getAnnotation(BaliseDb.class);
			if (baliseDb != null) {
				classDb.setChampDb(baliseDb.value(), new ChampDb(baliseDb.adapter(), field));
				classDb.putOrdre(baliseDb.value(), baliseDb.ordre());
			}
		}
		mapClasses.put(clazz, classDb);
	}

	/**
	 * Ecrit une ligne dans le ficheir CSV.
	 * 
	 * @param <Objet>
	 *            Objet associé au CSV.
	 * @param writerDb
	 *            writer à utiliser.
	 * @param nomChamps
	 *            liste des champs représentant l'entête du CSV.
	 * @param classDb
	 *            classe associée au fichier CSV.
	 * @param objet
	 *            objet à écrire dans le CSV.
	 * @throws IllegalAccessException
	 *             ne doit pas arriver.
	 */
	private <Objet> void writeLigne(AbstractWriterDb writerDb, List<String> nomChamps, ClassDb classDb,
			Objet objet) throws IllegalAccessException {
		List<String> champs = new ArrayList<String>();
		for (String nomChamp : nomChamps) {
			ChampDb champDb = classDb.getChampDb(nomChamp);
			champDb.getField().setAccessible(true);
			Object valeur = champDb.getField().get(objet);
			champDb.getField().setAccessible(false);
			if (valeur != null) {
				champs.add(champDb.getNewAdapterDb().toString(valeur));
			} else {
				champs.add(null);
			}
		}
		writerDb.writeLine(champs);
	}

	/**
	 * Ecrit un fichier CSV à partir d'une liste d'objet.
	 * 
	 * @param <Objet>
	 *            Objet associé au CSV.
	 * @param writer
	 *            writer représentant le fichier CSV.
	 * @param objets
	 *            liste des objets à écrire dans le fichier CSV.
	 * @param clazz
	 *            Classe associée au fichier CSV.
	 */
	public <Objet> void writeFile(Writer writer, Iterable<Objet> objets, Class<Objet> clazz) {
		try {
			final ClassDb classDb = mapClasses.get(clazz);
			AbstractWriterDb writerDb = factory.createWriterDb(writer, classDb.getSeparateurWithoutEscape());
			try {
				List<String> nomChamps = new ArrayList<String>();
				for (String champ : classDb.getNomChamps()) {
					nomChamps.add(champ);
				}
				Collections.sort(nomChamps, new Comparator<String>() {
					public int compare(String o1, String o2) {
						int thisVal = classDb.getOrdre(o1);
						int anotherVal = classDb.getOrdre(o2);
						return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
					}
				});
				writerDb.writeLine(nomChamps);
				for (Objet objet : objets) {
					writeLigne(writerDb, nomChamps, classDb, objet);
				}
			} finally {
				writerDb.close();
			}
		} catch (Exception exception) {
			throw new MoteurDbException(exception);
		}
	}
}
